# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: xguo

import enum
import functools
import math
import json
import logging
from typing import Union, List, Optional, Callable

import requests
from tornado import gen
from recordclass import recordclass

import coin.flow.topic_map as topic_map
import coin.flow.subscriber as flow_subscriber
import coin.proto.coin_order_enums_pb2 as coin_order
from coin.base.config import Config
from coin.base.param_util import to_list
from coin.base.timestamp import get_timestamp
from coin.exchange.base.order_fill_util import FillManager
from coin.exchange.base.order_gateway_logger import gen_og_log_request
from coin.exchange.base.order_id_util import OrderIdGenerator
from coin.exchange.kr_rest.product_holders import get_holder_from_product
from coin.exchange.kraken.kr_rest.constants import min_order_qty
from coin.exchange.kraken.kr_rest.currency import remove_native_currency_prefix
from coin.exchange.kraken.kr_rest.native_private_client import KrakenNativePrivateClient
from coin.exchange.kraken.kr_rest.private_client import KrakenPrivateParser
from coin.exchange.kraken.kr_rest.product import KrakenProduct, KrakenCurrency
from coin.exchange.kraken.order_gateway_log import KrakenOrderGatewayLogProvider
from coin.exchange.kraken.rest.private import KrakenPrivateQueryer
from coin.proto.coin_order_gateway_pb2 import OrderEvent, OrderGatewayConfig
from coin.strategy.hk_hard_arb.og_util import print_working_orders
from coin.exchange.base.rest.task_scheduler import Scheduler
from coin.exchange.base.order_gateway import (
    BaseOrderGatewayInfoMutator,
    Order,
    OrderGatewayBase,
    OrderGatewayStatus,
    OrderSide,
    OrderType,
)
from coin.proto.coin_query_pb2 import (
    CurrencyBalance,
    AccountBalance,
)
from coin.exchange.base.rest_order_gateway_base import (
    PrivateExchangeMessage,)
from coin.exchange.kraken.ws.ws_private_client import (
    KrakenWsPrivateClient,
    ChannelType,
)

KrakenOrderInternal = recordclass('KrakenOrderInternal',
                                  [
                                      'timestamp',
                                      'exchange_order_id',
                                      'cancel_confirmed',
                                      'cancel_sent_time',
                                      'fully_filled',
                                      'fill',
                                  ])


def create_kraken_order_internal(*,
                                 timestamp=None,
                                 exchange_order_id=None,
                                 cancel_confirmed=False,
                                 cancel_sent_time=None,
                                 fully_filled=False,
                                 fill=None):
  return KrakenOrderInternal(
      timestamp=timestamp,
      exchange_order_id=exchange_order_id,
      cancel_confirmed=cancel_confirmed,
      cancel_sent_time=cancel_sent_time,
      fully_filled=fully_filled,
      fill=fill,
  )


class KrakenOrderGatewayInfo(BaseOrderGatewayInfoMutator):
  CurrencyType = KrakenCurrency
  ProductType = KrakenProduct

  def __init__(self, currencies: List[KrakenCurrency], products: List[KrakenProduct]):
    super().__init__(currencies, products)
    self._clear_balance()

  def _clear_balance(self):
    for currency in self.currencies:
      balance = CurrencyBalance(currency=currency.currency,
                                currency_native=currency.native_currency,
                                total=0.,
                                available=0.,
                                hold=0.)
      self.set_balance(currency, 0.0, balance)

  def update_balance(self, update_msg):
    assert len(update_msg['error']) == 0
    self._clear_balance()
    for native_currency, balance in update_msg['result'].items():
      native_currency = remove_native_currency_prefix(native_currency)
      balance = float(balance)
      currency = KrakenCurrency.FromStrNativeCurrency(native_currency)
      currency_balance = CurrencyBalance(currency=currency.currency,
                                         currency_native=currency.native_currency,
                                         total=balance,
                                         available=balance,
                                         hold=0.0)
      self.set_balance(currency, balance, currency_balance)

  def balance_as_proto(self):
    return AccountBalance(exchange='Kraken',
                          market_type='Spot',
                          each_balance=self.get_notNone_balances_internal())


def get_order_submission_params(order: Order, post_only, order_formatter):
  assert order.order_id is not None, order
  side_map = {
      OrderSide.BUY: 'buy',
      OrderSide.SELL: 'sell',
  }
  params = {
      'pair': order.product.native_symbol,
      'side': side_map[order.order_side],
      'ordertype': 'limit',
      'price': order_formatter.format_price(order.price),
      'volume': order_formatter.format_qty(order.qty),
  }
  if post_only:
    params['oflags'] = 'post'
  return params


def create_order(
    product,
    price,
    qty,
    order_side,
    order_type,
    order_id,
    tag=None,
):
  timestamp = get_timestamp()
  fill = FillManager(price_orig=price, qty_orig=qty)
  order = Order(order_id=order_id,
                product=product,
                price=price,
                qty=qty,
                order_side=order_side,
                order_type=order_type,
                tag=tag,
                accepted=False,
                cancel_sent=False,
                internal=create_kraken_order_internal(timestamp=timestamp, fill=fill))
  return order


def create_order_from_order_update(order_id, exchange_order_id, order_info):
  side_map = {
      'buy': OrderSide.BUY,
      'sell': OrderSide.SELL,
  }
  type_map = {
      'limit': OrderType.LIMIT,
      'market': OrderType.MARKET,
  }
  price_orig = float(order_info['descr']['price'])
  qty_orig = float(order_info['vol'])
  qty_accum_fill = float(order_info['vol_exec'])
  tmp = order_info['descr']['order'].split()
  order_side = side_map[tmp[0]]
  order_type = type_map[tmp[4]]
  product = KrakenProduct.FromStr(order_info['descr']['pair'])
  fill = FillManager(qty_orig=qty_orig, price_orig=price_orig, qty_accum_fill=qty_accum_fill)
  order = Order(order_id=order_id,
                product=product,
                price=price_orig,
                qty=qty_orig,
                order_side=order_side,
                order_type=order_type,
                tag=None,
                accepted=True,
                cancel_sent=False,
                internal=create_kraken_order_internal(
                    timestamp=get_timestamp(),
                    exchange_order_id=exchange_order_id,
                    fill=fill,
                ))
  return order


class OrderManagerError(ValueError):
  pass


class OrderManager(object):
  def __init__(self):
    self._orders = {}  # All orders
    self._working_orders = {}
    self._orders_by_exchange_order_id = {}

  def _check_addable(self, new_order):
    if new_order.order_id is None:
      raise OrderManagerError('order id is None')
    if new_order.order_id in self._orders:
      raise OrderManagerError('Duplicated order id')
    if new_order.internal.exchange_order_id in self._orders_by_exchange_order_id:
      raise OrderManagerError('Duplicated exchange_order_id')

  def add_order(self, order: Order):
    self._check_addable(order)
    self._orders[order.order_id] = order
    if not (order.internal.cancel_confirmed or order.internal.fully_filled):
      self._working_orders[order.order_id] = order
    if order.internal.exchange_order_id is not None:
      self._orders_by_exchange_order_id[order.internal.exchange_order_id] = order

  def remove_order(self, order: Order, *, ignore_error=False):
    if order.order_id not in self._orders:
      raise OrderManagerError('remove_order error: order not found! %s' % order)

    if ignore_error:
      self._orders.pop(order.order_id, None)
    else:
      self._orders.pop(order.order_id)

    self._working_orders.pop(order.order_id, None)
    self._orders_by_exchange_order_id.pop(order.internal.exchange_order_id, None)

  def _set_exchange_order_id(self, order, exchange_order_id):
    # This method can only be called once for each order.
    assert isinstance(exchange_order_id, str), exchange_order_id
    assert order.accepted is False, order
    assert order.internal.exchange_order_id is None, order
    assert exchange_order_id not in self._orders_by_exchange_order_id
    order.accepted = True
    order.internal.exchange_order_id = exchange_order_id
    self._orders_by_exchange_order_id[exchange_order_id] = order

  def _set_cancel_confirmed(self, order, cancel_confirmed):
    assert cancel_confirmed in (True, False)
    assert order.internal.exchange_order_id is not None
    order.internal.cancel_confirmed = cancel_confirmed
    self._working_orders.pop(order.order_id, None)  # May removed already.

  def _set_fully_filled(self, order, fully_filled):
    assert fully_filled in (True, False)
    assert order.accepted is True, order
    assert order.internal.exchange_order_id is not None
    order.internal.fully_filled = fully_filled
    self._working_orders.pop(order.order_id, None)  # May removed already.

  def set_order(
      self,
      order: Order,
      *,
      exchange_order_id=None,
      fully_filled=None,
      cancel_confirmed=None,
  ):
    assert order.order_id is not None and order.order_id in self._orders, order

    # Only one of the parameters is specified, all others are None.
    params_set = {exchange_order_id, fully_filled, cancel_confirmed}
    assert None in params_set and len(params_set) == 2

    if exchange_order_id is not None:
      self._set_exchange_order_id(order, exchange_order_id)
    elif fully_filled is not None:
      self._set_fully_filled(order, fully_filled)
    elif cancel_confirmed is not None:
      self._set_cancel_confirmed(order, cancel_confirmed)

  def get_working_order(self):
    return list(self._working_orders.values())

  def filter(self, predicate: Callable, transform=None):
    transform = transform or (lambda x: x)
    res = []
    for order in self._orders.values():
      if predicate(order):
        res.append(transform(order))
    return res

  def get(self, *, order_id=None, exchange_order_id=None):
    # python style xor
    assert (order_id is None) != (exchange_order_id is None)

    if order_id is not None:
      return self._working_orders.get(order_id, None)
    elif exchange_order_id is not None:
      return self._orders_by_exchange_order_id.get(exchange_order_id, None)
    else:
      raise OrderManagerError('Invalid parameters for get method. %s, %s' %
                              (order_id, exchange_order_id))


class KrakenOrderGateway(OrderGatewayBase):
  _exchange = 'Kraken'
  _market_type = 'Spot'
  _api_version = None

  OGInfoType = KrakenOrderGatewayInfo

  class _MsgType(enum.IntEnum):
    WS_OPEN_ORDERS = 1
    WS_FILLS = 2
    WS_HEARTBEAT = 3

  def __init__(self,
               currencies: List[KrakenCurrency],
               products: Union[List[KrakenProduct], KrakenProduct],
               config: Config,
               og_config=None,
               logger=None):
    super().__init__()

    # config
    self._config = config

    # 15 as of 2018-xx-xx
    # 25 as of 2018-12-01
    default_og_config = OrderGatewayConfig(
        account_query_period=1,
        orders_snapshot_query_period=1,
        dead_orders_query_period=1,
        fill_query_period=1,
    )
    if og_config is not None:
      default_og_config.MergeFrom(og_config)
    self._og_config = default_og_config

    # product/balance/position
    self._og_info = KrakenOrderGatewayInfo(currencies, to_list(products))
    self._og_ready.set_status(OrderGatewayStatus.INITIALIZING)
    self._order_formatter = {
        product: get_holder_from_product(product).get_formatter()
        for product in self._og_info.products
    }

    # orders
    self._order_manager = OrderManager()
    self._order_id_generator = OrderIdGenerator()

    self._private_client = None
    self._private_queryer = None
    self._ws_private_client = None

    self._ioloop = None
    self._publisher = None
    self._flow_sub = None
    self._queue = None
    self._topic_strings = ['kraken_rest_private_1']

    self._processed_trades = set()
    self._balance_ready = False
    self._orders_ready = False
    self._scheduler = Scheduler()
    self._fee_info = None
    self._fee_info_update_time = None

    self._og_logger = KrakenOrderGatewayLogProvider(gen_og_log_request(config, self))
    self._logger = logger or logging.getLogger(__name__)

  def submittable(self):
    return self.is_ready()

  def get_working_order(self):
    return self._order_manager.get_working_order()

  def _assert_rest_update_msg(self, update_msg):
    if ('error' not in update_msg or 'result' not in update_msg or len(update_msg['error']) > 0):
      self._logger.error('Rest response error message: %s', update_msg)
      raise ValueError('Rest message error! %s' % update_msg)

  @gen.coroutine
  def _on_hit_order_rate_limit(self):
    self._private_queryer.stop()
    self._og_ready.set_reinit()
    self._logger.info('Order rate limit hit, disable OG for 30 seconds.')
    yield gen.sleep(30)  # stop for 30 seconds
    self._og_ready.set_status(OrderGatewayStatus.READY)
    self._private_queryer.start(self._ioloop, self._og_config)
    self._logger.info('Kraken OG is active again.')

  def _handle_submit_response(self, order, update_msg):
    try:
      self._assert_rest_update_msg(update_msg)
    except ValueError:
      if len(update_msg.get('error', [])) != 1:
        self._logger.error('Invalid error msg! %s', update_msg)
      elif update_msg.get('error')[0] == 'EAPI:Invalid nonce':
        self._logger.error('Invalid nonce error!')
      elif update_msg.get('error')[0] == 'EOrder:Insufficient funds':
        self._logger.error('Insufficient funds!')
      elif update_msg.get('error')[0] == 'EOrder:Rate limit exceeded':
        self._logger.error('Order rate limit exceeded!')
        self._ioloop.add_callback(self._on_hit_order_rate_limit)
      else:
        self._logger.exception('Submit error!')
        self._logger.error('order: %s', order)
      self._order_manager.remove_order(order, ignore_error=True)
      return
    if 'txid' not in update_msg['result']:
      self._logger.error('Invalid txid, update_msg=%s, order=%s', update_msg, order)
      self._order_manager.remove_order(order, ignore_error=True)
    else:
      exchange_order_id = update_msg['result']['txid'][0]
      prev_order = self._order_manager.get(exchange_order_id=exchange_order_id)
      if prev_order is None:
        self._order_manager.set_order(order, exchange_order_id=exchange_order_id)
      else:
        self._logger.info('order id replaced: %s => %s', prev_order.order_id, order.order_id)
        self._order_manager.remove_order(order)
        self._order_manager.remove_order(prev_order)
        prev_order.order_id = order.order_id
        self._order_manager.add_order(prev_order)

  def _handle_cancel_response(self, order, update_msg):
    try:
      self._assert_rest_update_msg(update_msg)
    except ValueError:
      if update_msg.get('error')[0] == 'EOrder:Unknown order':
        self._logger.info('Order is dead already, probably due to fill! %s', order)
      elif update_msg.get('error')[0] == 'EAPI:Invalid nonce':
        self._logger.error('Invalid nonce error!')
      else:
        self._logger.exception('Cancel error!')
        self._logger.error('order: %s', order)
      return
    if order.internal.cancel_confirmed or order.internal.fully_filled:
      self._logger.info(
          'Order is marked dead already, probably due to query_close_orders response comes earlier. %s',
          order)
    elif update_msg['result']['count'] == 1:
      self._order_manager.set_order(order, cancel_confirmed=True)
    else:
      self._logger.error('Cancel count is not 1. %s', update_msg)

  def _handle_account_response(self, update_msg):
    self._assert_rest_update_msg(update_msg)
    self._og_info.update_balance(update_msg)
    self._balance_ready = True
    if self._balance_ready and self._orders_ready:
      self._og_ready.set_status(OrderGatewayStatus.READY)
    self._og_logger.write_balance(self._og_info.balance_as_proto())

  def _handle_open_orders_response(self, update_msg):
    self._assert_rest_update_msg(update_msg)
    timestamp = get_timestamp()
    live_exchange_order_ids = []
    for exchange_order_id, order_info in update_msg['result']['open'].items():
      assert 'status' in order_info and order_info['status'] in ('open', 'pending'), order_info
      live_exchange_order_ids.append(exchange_order_id)
      order = self._order_manager.get(exchange_order_id=exchange_order_id)
      if order is None:
        # Treat as foreign order.
        order_id = self._order_id_generator.gen_order_id(is_foreign=True)
        order = create_order_from_order_update(order_id, exchange_order_id, order_info)
        if order.product in self._og_info.products:
          self._order_manager.add_order(order)
          self._logger.info('Add foreign order: %s', order)
        else:
          self._logger.debug('Product we do not care: %s', order)
      else:
        if order.cancel_sent and timestamp - order.internal.cancel_sent_time > 5e9:
          order.cancel_sent = False
        if (order.accepted is not True or order.internal.cancel_confirmed is True
            or order.internal.fully_filled is True):
          self._logger.error(
              'Order state is incorrect. Sometimes order can appear in both open order list and closed order list\n. %s\n%s',
              order,
              order_info)

    for order in self.get_working_order():
      if (order.internal.exchange_order_id is not None
          and order.internal.exchange_order_id not in live_exchange_order_ids
          and timestamp - order.internal.timestamp > 30e9):
        # Treat as canceled order.
        self._order_manager.set_order(order, cancel_confirmed=True)
        self._logger.info('order removed since it is not in the open order list. %s', order)

    self._orders_ready = True
    if self._balance_ready and self._orders_ready:
      self._logger.info('Kraken OG is ready')
      self._og_ready.set_status(OrderGatewayStatus.READY)

  def _handle_closed_orders_response(self, update_msg):
    self._assert_rest_update_msg(update_msg)
    for exchange_order_id, order_info in update_msg['result']['closed'].items():
      status = order_info['status']  # open, closed, canceled
      assert status in ('closed', 'canceled'), status
      order = self._order_manager.get(exchange_order_id=exchange_order_id)
      if order is None:
        # Ignore dead order that is not in the order manager.
        pass
      elif order.internal.cancel_confirmed or order.internal.fully_filled:
        # Ignore dead order.
        pass
      else:
        if order.cancel_sent:
          if status == 'closed':
            self._logger.info('Cancel is sent, but order is closed due to full fill.')
            self._order_manager.set_order(order, fully_filled=True)
          else:
            self._order_manager.set_order(order, cancel_confirmed=True)
        else:
          if status == 'closed':
            self._order_manager.set_order(order, fully_filled=True)
          else:
            self._order_manager.set_order(order, cancel_confirmed=True)
            self._logger.info('Cancel not sent, order is canceled by other client.')

  def _handle_trade_history_response(self, update_msg):
    self._assert_rest_update_msg(update_msg)
    for trade_id, trade_info in update_msg['result']['trades'].items():
      if trade_id not in self._processed_trades:
        self._handle_trade(trade_id, trade_info)

  def _handle_trade(self, trade_id, trade_info):
    timestamp = get_timestamp()
    exchange_order_id = trade_info['ordertxid']
    price_last_fill = float(trade_info['price'])
    qty_last_fill = float(trade_info['vol'])
    order = self._order_manager.get(exchange_order_id=exchange_order_id)
    if order is None:
      self._logger.error('Received fill message')
    else:
      self._processed_trades.add(trade_id)
      order.internal.fill.update_by_last_fill(price_last_fill, qty_last_fill)
      self._publish_fill(price_last_fill, qty_last_fill, order)
      order.internal.fill.log_last_fill(order, self._logger)
      fee_rate = float(trade_info['fee']) / (float(trade_info['cost']) + 1e-10)
      self._handle_fill_event(order, fee_rate, timestamp)

  @gen.coroutine
  def _handle_fill_event(self, order, fee_rate, timestamp):
    fill_type = self.find_fill_type(order.product, fee_rate)
    if fill_type == coin_order.UNKNOWN_FILL_TYPE:
      if self._fee_info_update_time is None or timestamp - self._fee_info_update_time > 30e9:
        self._fee_info_update_time = timestamp
        try:
          pairs = [product.native_symbol for product in self._og_info.products]
          response = yield self._private_client.query_trade_volume(pairs, fee_info=True)
          self._fee_info = KrakenPrivateParser.parse_fee_rate(response.json())
          self._logger.info('trade volume info: %s', self._fee_info)
        except Exception:
          self._logger.error('fee info not available')
        fill_type = self.find_fill_type(order.product, fee_rate)

    self._og_logger.gen_order_event_and_log(order, timestamp, OrderEvent.ORDER_FILLED, fill_type)

  def _handle_ws_fills(self, exchange_message: PrivateExchangeMessage):
    for dict_item in exchange_message.data[0]:
      (key, value), = dict_item.items()
      self._handle_trade(key, value)

  def _prepare_ws_private_client(self, start=True):
    if self._ws_private_client is not None:
      self._ws_private_client.stop()

    close_callback = functools.partial(self._prepare_ws_private_client, True)
    self._ws_private_client = KrakenWsPrivateClient(
        self._config.key_file,
        ioloop=self._ioloop,
        notification_callback=self._notification_callback,
        close_callback=close_callback,
        on_ready=self._on_ws_private_client_ready,
    )
    if start:
      self._ws_private_client.start()

  def _notification_callback(self, ws_msg):
    timestamp = get_timestamp()
    feed_type = ChannelType(ws_msg[-1])
    if feed_type == ChannelType.OPEN_ORDERS:
      msg_type = self._MsgType.WS_OPEN_ORDERS
    elif feed_type == ChannelType.OWN_TRADES:
      msg_type = self._MsgType.WS_FILLS
    elif feed_type == ChannelType.HEARTBEAT:
      msg_type = self._MsgType.WS_HEARTBEAT
    else:
      self._logger.error('Unhandled message type: %s', ws_msg)
      return

    exchange_message = PrivateExchangeMessage(msg_type=msg_type,
                                              data=ws_msg,
                                              received_timestamp=timestamp,
                                              request_params=None,
                                              request_timestamp=None)
    self._publisher.write(timestamp, exchange_message)

  @gen.coroutine
  def _on_ws_private_client_ready(self):
    yield self._ws_private_client.subscribe(ChannelType.OWN_TRADES)

  def on_rest_private_msg(self, record, queue_data, topic_data):
    msg = record.data
    if isinstance(msg, PrivateExchangeMessage):
      if msg.data[-1] == "ownTrades":
        self._handle_ws_fills(msg)
      return
    if 'error' not in msg['data'] or 'result' not in msg['data'] or len(msg['data']['error']) > 0:
      if msg['data'].get('error') == ['EAPI:Invalid nonce']:
        self._logger.error('Invalid nonce error!')
      else:
        self._logger.error('Rest response error message: %s', msg)
      return

    if msg['query'] == 'query_account_balance':
      self._handle_account_response(msg['data'])
    elif msg['query'] == 'query_open_orders':
      self._handle_open_orders_response(msg['data'])
    elif msg['query'] == 'query_closed_orders':
      self._handle_closed_orders_response(msg['data'])
    elif msg['query'] == 'query_trades_history':
      self._handle_trade_history_response(msg['data'])
    else:
      self._logger.error('Unknown rest message: %s', msg)

  def start(self, queue, ioloop, flow_sub=None):
    self._prepare_ws_private_client(start=True)
    self._private_client = KrakenNativePrivateClient(key_file=self._config.key_file,
                                                     sync=False,
                                                     use_extra_keys=self._og_config.use_extra_keys)
    self._ioloop = ioloop
    self._queue = queue
    self._private_queryer = KrakenPrivateQueryer(
        queue=queue,
        worker_id='1',
        private_client=self._private_client,
    )
    self._private_queryer.start(ioloop, self._og_config)
    topic_id = topic_map.search_topic_id('kraken_rest_private_1')[0][1]
    self._publisher = queue.get_writer(topic_id=topic_id)
    self._logger.info('num_of_keys used: %s', self._private_client.num_of_keys)

    def print_info():
      print_working_orders(self._logger, self)

    self._scheduler.add_periodic_task(10, print_info)
    self._scheduler.start()

    # Subscribe to flow for feed.
    self._flow_sub = flow_sub or flow_subscriber.from_queue(queue)
    self._flow_sub.subscribe(self._topic_strings[0], self.on_rest_private_msg)

  def find_fill_type(self, product, fee_rate):
    if self._fee_info is None:
      return coin_order.UNKNOWN_FILL_TYPE

    rel_tol = 1e-4
    abs_tol = 1e-8
    if math.isclose(fee_rate, self._fee_info[product]['maker'], rel_tol=rel_tol, abs_tol=abs_tol):
      return coin_order.MAKER_FILL_TYPE
    elif math.isclose(fee_rate, self._fee_info[product]['taker'], rel_tol=rel_tol, abs_tol=abs_tol):
      return coin_order.TAKER_FILL_TYPE
    else:
      return coin_order.UNKNOWN_FILL_TYPE

  def submit_impl(
      self,
      product: KrakenProduct,
      price: float,
      qty: float,
      order_side: OrderSide,
      order_type: OrderType = OrderType.LIMIT,  # LIMIT
      order_id: Optional[str] = None,
      tag=None,
      post_only=False):
    min_qty = min_order_qty[product.base.currency]
    assert isinstance(product, KrakenProduct)
    assert self.is_ready()
    assert price > 0
    if qty < min_qty:
      self._logger.warning('Small qty: %f < %f', qty, min_qty)
      return

    if order_type != OrderType.LIMIT:
      raise NotImplementedError()

    order_id = order_id or self._order_id_generator.gen_order_id()
    order = create_order(product=product,
                         price=price,
                         qty=qty,
                         order_side=order_side,
                         order_type=order_type,
                         order_id=order_id,
                         tag=None)
    self._order_manager.add_order(order)
    self._ioloop.add_callback(self._submit_impl, order, post_only)

  @gen.coroutine
  def _submit_impl(self, order, post_only):
    timestamp = get_timestamp()
    self._og_logger.gen_order_submitted_event_and_log(order, timestamp)
    success = False
    try:
      order_formatter = self._order_formatter[order.product]
      params = get_order_submission_params(order, post_only, order_formatter)
      response = yield self._private_client.query_add_standard_order(**params)
      if response.status_code != 200:
        if response.status_code in (502, 520):
          self._logger.error('Invalid status code: %s', response.status_code)
        else:
          self._logger.error('Invalid status code: %s (%s)', response.status_code, response.content)
        self._order_manager.remove_order(order)
        return
      msg = json.loads(response.content)
      self._handle_submit_response(order, msg)
      success = True
    except json.JSONDecodeError:
      self._logger.exception('JsonDecodeException: %s', response.content)
      self._order_manager.remove_order(order)
    except requests.exceptions.RequestException:
      self._logger.exception('RequestException')
      self._order_manager.remove_order(order)
    except Exception:
      self._logger.exception('Unknown Exception')
      self._order_manager.remove_order(order, ignore_error=True)

    self._og_logger.gen_order_response_and_order_event_and_log(
        symbol=order.product.symbol,
        success=success,
        timestamp=get_timestamp(),
        internal_order_id=order.order_id,
    )
    elapsed_time = get_timestamp() - timestamp
    self._logger.debug('Order submit elapsed time:  %.4f sec', elapsed_time / 1e9)

  def cancel_impl(self, order_id, *, ignore_error=False):
    assert self.is_ready()
    timestamp = get_timestamp()

    order = self._order_manager.get(order_id=order_id)
    if order is None:
      self._logger.error('No order_id=%s', order_id)
      return

    if not order.accepted:
      self._logger.error('order_id=%s has not been accepted, %s', order_id, order)
      return

    if order.cancel_sent:
      return

    order.cancel_sent = True
    order.internal.cancel_sent_time = timestamp
    self._ioloop.add_callback(self._cancel_impl, order, ignore_error)

  @gen.coroutine
  def _cancel_impl(self, order, ignore_error):
    timestamp = get_timestamp()
    self._og_logger.gen_cancel_request_and_order_event_and_log(
        symbol=order.product.symbol,
        internal_order_id=order.order_id,
        timestamp=timestamp,
    )
    success = False
    try:
      response = yield self._private_client.query_cancel_open_order(
          order.internal.exchange_order_id)
      if response.status_code != 200:
        if response.status_code in (502, 520):
          self._logger.error('Invalid status code: %s', response.status_code)
        else:
          self._logger.error('Invalid status code: %s (%s)', response.status_code, response.content)
        order.cancel_sent = False
        return
      msg = json.loads(response.content)
      self._handle_cancel_response(order, msg)
      success = True
    except json.JSONDecodeError:
      self._logger.exception('JsonDecodeException: %s', response.content)
      self._order_manager.remove_order(order)
    except requests.exceptions.RequestException:
      self._logger.exception('RequestException')
      self._order_manager.remove_order(order)
    except Exception:
      self._logger.exception('Unknown Exception')
      self._order_manager.remove_order(order)

    self._og_logger.gen_cancel_response_and_log(
        symbol=order.product.symbol,
        internal_order_id=order.order_id,
        success=success,
    )
    elapsed_time = get_timestamp() - timestamp
    self._logger.debug('Order cancel elapsed time:  %.4f sec', elapsed_time / 1e9)

  def cancel_multiple_impl(self, order_ids, *, ignore_error=False):
    for n, order_id in enumerate(order_ids):
      self.cancel(order_id, ignore_error=ignore_error)

  def cancel_all_impl(self, *, ignore_error=False):
    order_ids = [order.order_id for order in self.get_working_order()]
    self.cancel_multiple(order_ids, ignore_error=ignore_error)

  def cancel_product_impl(self, product, *, ignore_error):
    order_ids = [order.order_id for order in self.get_working_order() if order.product == product]
    self.cancel_multiple(order_ids, ignore_error=ignore_error)
